/*
 Xern Entity Relocation Network
 Copyright (C) 2011 Ronie Salgado <roniesalg@gmail.com>

 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 in the Software without restriction, including without limitation the rights
 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 copies of the Software, and to permit persons to whom the Software is
 furnished to do so, subject to the following conditions:

 The above copyright notice and this permission notice shall be included in
 all copies or substantial portions of the Software.

 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
*/

#include "Xern/Log.hpp"
#include "Xern/Exceptions.hpp"
#include "Xern/Engine.hpp"
#include "Xern/IClientModule.hpp"
#include "Xern/WorldManager.hpp"

#include <boost/bind.hpp>
#include <boost/uuid/uuid_generators.hpp>
#include <xmlrpc-c/girerr.hpp>
#include "XmlRpcHandler.hpp"
#include "MetaverseClient.hpp"
#include "RemoteRegion.hpp"
#include "RemoteObject.hpp"
#include "RemotePlayer.hpp"

namespace XernMetaverse
{
	namespace rpc = xmlrpc_c;
	const posix_time::time_duration MetaverseClient::ClientThreadInterval = posix_time::milliseconds(20);

	MetaverseClient::MetaverseClient()
	{
		loginServer = "http://localhost:9000/";
	}

	MetaverseClient::~MetaverseClient()
	{
	}

	void MetaverseClient::Initialize()
	{
		MetaverseNode::Initialize();
	}

	void MetaverseClient::Shutdown()
	{
		if(session)
			Logout();
		MetaverseNode::Shutdown();
	}

	void MetaverseClient::SetLoginServer(const std::string &uri)
	{
		loginServer = uri;
	}

	const std::string &MetaverseClient::GetLoginServer() const
	{
		return loginServer;
	}

	inline const rpc::value *GetResponseValue(const std::map<std::string,
			rpc::value> &response, const std::string &name)
	{
		std::map<std::string, rpc::value>::const_iterator it = response.find(name);
		if (it != response.end())
		{
			return &it->second;
		}
		return NULL;
	}

	inline std::string GetResponseField(
			const std::map<std::string, rpc::value> &response,
			const std::string &name)
	{
		std::map<std::string, rpc::value>::const_iterator it = response.find(name);
		if (it != response.end())
		{
			if (it->second.type() == rpc::value::TYPE_STRING)
			{
				return std::string(rpc::value_string(it->second));
			}
		}
		return std::string(); // Return empty string.
	}

	bool MetaverseClient::Login(const std::string &firstName,
			const std::string &lastName, const std::string &password)
	{
		boost::uuids::string_generator uuidParser;

		// Lock the client
		boost::unique_lock<boost::mutex> lock(nodeMutex);

		if(!ReadTemplates())
			return false;

		// Store the login rpc call data
		std::map<std::string, rpc::value> loginData;
		loginData["first"] = rpc::value_string(firstName);
		loginData["last"] = rpc::value_string(lastName);
		loginData["passwd"] = rpc::value_string(password);
		loginData["user-agent"] = rpc::value_string("XernBot");
		loginData["version"] = rpc::value_string("XernBot 0.1a");
		loginData["platform"] = rpc::value_string("Lin");
		loginData["start"] = rpc::value_string("last");
		//loginData["mac"] = "";
		//loginData["agree_to_tos"] = rpc::value_string("true");
		//loginData["read_critical"] = rpc::value_string("false");

		rpc::value_struct loginArg(loginData);
		rpc::value result;

		// Perform login call.
		rpc::paramList paramList;
		paramList.add(loginArg);
		try
		{
			XmlRpcHandler rpcHandler(httpClient);
			rpcHandler.Invoke(loginServer, "login_to_simulator", paramList, &result);
		}
		catch(const HttpException &e)
		{
			LogConsole->Error("Login failure: %s", e.what());
			return false;
		}
		catch (const girerr::error & e)
		{
			LogConsole->Error("Login failure: %s", e.what());
			return false;
		}

		if (result.type() != rpc::value::TYPE_STRUCT)
		{
			LogConsole->Error("Failed to perform login xml-rpc call.");
			return false;
		}

		// Extract the result output.
		rpc::value_struct const sresult(result);
		std::map<std::string, rpc::value> const resultData(sresult);

		// Check the login response
		std::string login_response = GetResponseField(resultData, "login");
		if (login_response.empty() || login_response == "false")
		{
			LogConsole->Error("Failed to log in.");
			return false;
		}

		// Read important fields.
		std::string sessionIdS = GetResponseField(resultData, "session_id");
		std::string secureSessionIdS = GetResponseField(resultData,
				"secure_session_id");
		std::string agentIdS = GetResponseField(resultData, "agent_id");
		std::string simIpS = GetResponseField(resultData, "sim_ip");
		const rpc::value *portV = GetResponseValue(resultData, "sim_port");
		const rpc::value *circuitCodeV = GetResponseValue(resultData, "circuit_code");
		if (sessionIdS.empty() || secureSessionIdS.empty() || agentIdS.empty()
			|| simIpS.empty() || !portV || !circuitCodeV)
		{
			LogConsole->Error("Failed to log in. Incomplete server response.");
			return false;
		}

		// Check if the fields are valid.
		if (portV->type() != rpc::value::TYPE_INT ||
			circuitCodeV->type() != rpc::value::TYPE_INT)
		{
			LogConsole->Error("Failed to log in. Invalid server response.");
			return false;
		}

		printf("SessionID: %s\n", sessionIdS.c_str());
		printf("AgentID: %s\n", agentIdS.c_str());
		int circuitCode = *((const rpc::value_int*)circuitCodeV);
		printf("CircuitCode %d\n", circuitCode);

		// Create the client session.
		session.reset(new Session());
		session->circuitCode = circuitCode;
		session->id = uuidParser(sessionIdS);
		session->agentId = uuidParser(agentIdS);

		// Print the welcome message.
		std::string loginMsg = GetResponseField(resultData, "message");
		if(!loginMsg.empty())
			LogConsole->Notify("%s", loginMsg.c_str());

        // Resolve the endpoint.
        int port = *((const rpc::value_int*)portV);
        char portStr[32];
        sprintf(portStr, "%d", port);
        udp::resolver resolver(ioService);
        udp::resolver::query query(udp::v4(), simIpS, portStr);
        session->endpoint = *resolver.resolve(query);

        // Change the session state
        session->state = Session::SS_CONNECTING_WACK;

        // Register the session.
        sessions.insert(std::make_pair(session->endpoint, session));

        // Send a 'UseCircuitCode' message.
        {
        	boost::shared_ptr<Message> message = CreateMessage("UseCircuitCode");
        	assert(message);
        	MessageBlock &circuitCodeBlock = message->GetBlock("CircuitCode");

        	// Set the fields.
        	circuitCodeBlock.GetVariable("Code").SetValue<u32> (circuitCode);
        	circuitCodeBlock.GetVariable("SessionID").SetValue<uuid> (session->id);
        	circuitCodeBlock.GetVariable("ID").SetValue<uuid> (session->agentId);

        	// Send the message.
        	message->isReliable = true;
        	session->Send(message);
        }

        // Wait the login to finish.
        loginCompleted.wait(lock);

        // Check the new client state.
        if(session->state != Session::SS_CONNECTED)
        {
        	LogConsole->Error("Failed to login");
        	return false;
        }

        // Create the session state
        sessionState.reset(new ClientSession(session, shared_from_this()));
        sessionState->Connected(regionHandshake);

        // Start the client thread.
        boost::thread ct(boost::bind(&MetaverseClient::ClientThread, this));
        clientThread.swap(ct);

        // Send a throttle message
        SendThrottle();

        // Print a message.
        LogConsole->Notify("LoginComplete. Online!");

		return true;
	}

	void MetaverseClient::Logout()
	{
		boost::unique_lock<boost::mutex> lock(nodeMutex);

		// Check the client session.
		if(!session || session->state == Session::SS_DISCONNECTED)
			return;

		// Change the client state.
		session->sessionMutex.lock();
		session->state = Session::SS_LOGOUT;
		session->sessionMutex.unlock();

		// Send a logout request.
        // Create the message.
        boost::shared_ptr<Message> message = CreateMessage("LogoutRequest");
        assert(message);
        MessageBlock &agentDataBlock = message->GetBlock("AgentData");

        // Set the fields.
        agentDataBlock.GetVariable("AgentID").SetValue<uuid> (session->agentId);
        agentDataBlock.GetVariable("SessionID").SetValue<uuid> (session->id);

        // Send the message.
        message->isReliable = true;
        session->Send(message);

        // Wait until a response.
        logoutCompleted.wait(lock);

        // Destroy the client session.
        sessions.erase(session->endpoint);

        // Wait the client thread to terminate.
        clientThread.join();
        session.reset();

        // Notify the client state.
        sessionState->Disconnected();
    }

	void MetaverseClient::ChatMessage(const std::string &message)
	{
		if(!session)
			throw XernException("Can't send chat message when not connected.");

        boost::shared_ptr<Message> netMessage = CreateMessage("ChatFromViewer");
        assert(netMessage);

        netMessage->isReliable = true;

        MessageBlock &agentData = netMessage->GetBlock("AgentData");
        agentData.GetVariable("AgentID").SetValue<uuid> (session->agentId);
        agentData.GetVariable("SessionID").SetValue<uuid> (session->id);

        MessageBlock &chatData = netMessage->GetBlock("ChatData");
        chatData.GetVariable("Message").SetValue<std::string> (message);
        chatData.GetVariable("Type").SetValue<unsigned int> (3);
        chatData.GetVariable("Channel").SetValue<signed int> (0);

        session->Send(netMessage);
	}

    void MetaverseClient::SendMessage(boost::shared_ptr<Message> message)
    {
    	if(session)
    		session->Send(message);
    }

    void MetaverseClient::SendThrottle()
    {
    	// Create the message.
    	boost::shared_ptr<Message> message = CreateMessage("AgentThrottle");

    	// Fill the agent data block.
    	MessageBlock &agentDataBlock = message->GetBlock("AgentData");
    	agentDataBlock.GetVariable("AgentID").SetValue<uuid> (session->agentId);
    	agentDataBlock.GetVariable("SessionID").SetValue<uuid> (session->id);
    	agentDataBlock.GetVariable("CircuitCode").SetValue<unsigned int> (session->circuitCode);

    	// Fill the throttle block.
    	MessageBlock &throttleBlock = message->GetBlock("Throttle");
    	throttleBlock.GetVariable("GenCounter").SetValue<unsigned int> (10);

    	// Fill the throttles type
    	// TODO: Read those values from a config file.
    	MessageVariable &throttlesVar = throttleBlock.GetVariable("Throttles");
    	const size_t size = 28;
    	throttlesVar.SetBufferSize(size);
    	DataStream out(size, throttlesVar.GetBuffer());
    	const float DefaultThrottle = 200000;
    	out << DefaultThrottle; // Resend
    	out << DefaultThrottle; // Land
    	out << DefaultThrottle; // Wind
    	out << DefaultThrottle; // Cloud
    	out << DefaultThrottle; // Task
    	out << DefaultThrottle; // Texture
    	out << DefaultThrottle; // Asset

    	// Send the throttle message.
    	session->Send(message);
    }

	bool MetaverseClient::OnReceiveMessage(boost::shared_ptr<Session> session,
			boost::shared_ptr<Message> message)
	{
		// Only process urgent messages.
		switch(session->state)
		{
		case Session::SS_CONNECTING_WACK:
			// Check the waiting acks.
			if(session->packetsWaiting.size() == 0)
			{
				// Change the session state.
				session->state = Session::SS_CONNECTING_WRESP;

				// Send a 'CompleteAgentMovement' message.
				boost::shared_ptr<Message> newMessage = CreateMessage("CompleteAgentMovement");
				assert(newMessage);
				MessageBlock &agentDataBlock = newMessage->GetBlock("AgentData");

				// Set the fields.
				agentDataBlock.GetVariable("AgentID").SetValue<uuid> (session->agentId);
				agentDataBlock.GetVariable("SessionID").SetValue<uuid> (session->id);
				agentDataBlock.GetVariable("CircuitCode").SetValue<u32> (session->circuitCode);

				// Send the message.
				newMessage->isReliable = true;
				session->sendQueue.push_back(newMessage);
				return false;
			}
			break;
		case Session::SS_CONNECTING_WRESP:
			// Check the message type.
			if(message->GetTemplate()->name == "RegionHandshake")
			{
				// Change the session state.
				session->state = Session::SS_CONNECTED;

				// Send a 'RegionHanshakeReply' message.
				boost::shared_ptr<Message> newMessage = CreateMessage("RegionHandshakeReply");
				assert(newMessage);

				// Set the fields.
				MessageBlock &agentDataBlock = newMessage->GetBlock("AgentData");
				agentDataBlock.GetVariable("AgentID").SetValue<uuid> (session->agentId);
				agentDataBlock.GetVariable("SessionID").SetValue<uuid> (session->id);

				MessageBlock &regionInfo = newMessage->GetBlock("RegionInfo");
				regionInfo.GetVariable("Flags").SetValue<u32> (0);

				// Send the message.
				newMessage->isReliable = true;
				session->sendQueue.push_back(newMessage);

				// Store the region handshake message.
				regionHandshake = message;

				// Notify the login thread.
				loginCompleted.notify_one();
				return true;
			}
			break;
		case Session::SS_LOGOUT:
			// Check the message type.
			if(message->GetTemplate()->name == "LogoutReply")
			{
				// Change the session state.
				session->state = Session::SS_DISCONNECTED;

				// Notify the logout thread.
				logoutCompleted.notify_one();
				return true;
			}
			break;
		case Session::SS_CONNECTED:
		case Session::SS_DISCONNECTED:
		default:
			break;
		}

		return false;
	}

	// Xern tunnel.
	void MetaverseClient::Frame(float delta)
	{
		if(sessionState)
			sessionState->Frame(delta);
	}

	boost::shared_ptr<Entity>
	MetaverseClient::CreatePlayerController(boost::shared_ptr<Region> region)
	{
		if(!sessionState)
			return boost::shared_ptr<Entity> ();
		return sessionState->CreatePlayerController(region);
	}

	// Client thread
	void MetaverseClient::ClientThread()
	{
		// Create a copy of the session.
		boost::shared_ptr<Session> session = this->session;

		// Make a of the session state.
		boost::shared_ptr<ClientSession> state = sessionState;

		// Return if there isn't a session.
		if(!session)
			return;

		while(session->state == Session::SS_CONNECTED)
		{
			// Don't eat all of the cpu.
			boost::this_thread::sleep(ClientThreadInterval);

			// Receive the session messages.
			boost::shared_ptr<Message> message = session->Receive();
			for(; message; message = session->Receive())
			{
				const std::string &name = message->GetTemplate()->name;

				// Send the message into the region.
				state->ReceiveMessage(message);
			}
		}
	}

}; // namespace XernMetaverse
